From: Jonas Dreßler Date: Wed, 3 Aug 2022 13:01:52 +0000 (+0200) Subject: dnd: Use ClutterGesture to start DnD interactions X-Git-Tag: archive/raspbian/50.5-1+rpi1~1^2~104^2^2~34 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=bab6f8cc87bc1d5bced72f272a6adb3859209ee5;p=gnome-shell.git dnd: Use ClutterGesture to start DnD interactions With the last commit we introduced ShellDndStartGesture, so now port the Draggable to actually use that gesture, and to move over to a ClutterGrab once the DndStartGesture has recognized. This is shuffling a lot of things around and removing quite a bit of code. One thing to note is that to allow users of the DndStart gesture to specify its relationship with other gestures (eg. to allow it run together with a LongPressGesture), the DndStartGesture is exposed on the draggable as the startGesture property. Also, restore the integration with WindowPreview gestures, so all of them are able to cooperate. This used to be done via addClickAction() in the past, we can now specify this interaction to ClutterGesture. Part-of: --- diff --git a/js/ui/dnd.js b/js/ui/dnd.js index c41e0f8ca..e90b69ab1 100644 --- a/js/ui/dnd.js +++ b/js/ui/dnd.js @@ -23,13 +23,6 @@ export const DragMotionResult = { CONTINUE: 3, }; -/** @enum {number} */ -const DragState = { - INIT: 0, - DRAGGING: 1, - CANCELLED: 2, -}; - const DRAG_CURSOR_MAP = { 0: Meta.Cursor.NO_DROP, 1: Meta.Cursor.COPY, @@ -95,7 +88,6 @@ export function removeDragMonitor(monitor) { class _Draggable extends Signals.EventEmitter { constructor(actor, params) { super(); - params = Params.parse(params, { manualMode: false, timeoutThreshold: 0, @@ -105,23 +97,14 @@ class _Draggable extends Signals.EventEmitter { }); this.actor = actor; - this._dragState = DragState.INIT; - - if (!params.manualMode) { - this.actor.connect('button-press-event', - this._onButtonPress.bind(this)); - this.actor.connect('touch-event', - this._onTouchEvent.bind(this)); - } - this.actor.connect('destroy', () => { - this._actorDestroyed = true; - - if (this._dragState === DragState.DRAGGING && this._dragCancellable) - this._cancelDrag(global.get_current_time()); + this._dndGesture = new St.DndStartGesture({ + manual_mode: params.manualMode, + timeout_threshold: params.timeoutThreshold, }); - this._onEventId = null; - this._touchSequence = null; + this._dndGesture.connect('recognize', () => this._gestureRecognized()); + + this.actor.add_action(this._dndGesture); this._restoreOnSuccess = params.restoreOnSuccess; this._dragActorMaxSize = params.dragActorMaxSize; @@ -132,204 +115,23 @@ class _Draggable extends Signals.EventEmitter { this._dragCancellable = true; } - /** - * addClickAction: - * - * @param {Clutter.ClickAction} action - click action to add to draggable actor - * - * Add @action to the draggable's actor, and set it up so that it does not - * impede drag operations. - */ - addClickAction(action) { - action.connect('clicked', () => (this._actionClicked = true)); - action.connect('long-press', (a, actor, state) => { - if (state !== Clutter.LongPressState.CANCEL) - return true; - - const event = Clutter.get_current_event(); - this._dragTouchSequence = event.get_event_sequence(); - - if (this._longPressLater) - return true; - - // A click cancels a long-press before any click handler is - // run - make sure to not start a drag in that case - const laters = global.compositor.get_laters(); - this._longPressLater = laters.add(Meta.LaterType.BEFORE_REDRAW, () => { - delete this._longPressLater; - if (this._actionClicked) { - delete this._actionClicked; - return GLib.SOURCE_REMOVE; - } - action.release(); - this.startDrag( - ...action.get_coords(), - event.get_time(), - this._dragTouchSequence, - event.get_device()); - - return GLib.SOURCE_REMOVE; - }); - return true; - }); - - this.actor.add_action(action); - } - - _onButtonPress(actor, event) { - if (event.get_button() !== 1) - return Clutter.EVENT_PROPAGATE; - - this._grabActor(event.get_device()); - - let [stageX, stageY] = event.get_coords(); - this._dragStartX = stageX; - this._dragStartY = stageY; - this._dragStartTime = event.get_time(); - this._dragThresholdIgnored = false; - - return Clutter.EVENT_PROPAGATE; - } - - _onTouchEvent(actor, event) { - // We only handle touch events here on wayland. On X11 - // we do get emulated pointer events, which already works - // for single-touch cases. Besides, the X11 passive touch grab - // set up by Mutter will make us see first the touch events - // and later the pointer events, so it will look like two - // unrelated series of events, we want to avoid double handling - // in these cases. - if (!Meta.is_wayland_compositor()) - return Clutter.EVENT_PROPAGATE; - - if (event.type() !== Clutter.EventType.TOUCH_BEGIN || - !global.display.is_pointer_emulating_sequence(event.get_event_sequence())) - return Clutter.EVENT_PROPAGATE; - - this._grabActor(event.get_device(), event.get_event_sequence()); - this._dragStartTime = event.get_time(); - this._dragThresholdIgnored = false; - - let [stageX, stageY] = event.get_coords(); - this._dragStartX = stageX; - this._dragStartY = stageY; - - return Clutter.EVENT_PROPAGATE; - } - - _grabDevice(actor, pointer, touchSequence) { - this._grab = global.stage.grab(actor); - this._grabbedDevice = pointer; - this._touchSequence = touchSequence; - } - - _ungrabDevice() { - if (this._grab) { - this._grab.dismiss(); - this._grab = null; - } - this._touchSequence = null; - this._grabbedDevice = null; - } - - _grabActor(device, touchSequence) { - this._grabDevice(this.actor, device, touchSequence); - this._onEventId = this.actor.connect('event', - this._onEvent.bind(this)); - } - - _ungrabActor() { - if (!this._onEventId) - return; - - this._ungrabDevice(); - this.actor.disconnect(this._onEventId); - this._onEventId = null; - } - _grabEvents(device, touchSequence) { - if (!this._eventsGrab) { - let grab = Main.pushModal(_getEventHandlerActor()); - if ((grab.get_seat_state() & Clutter.GrabState.POINTER) !== 0) { - this._grabDevice(_getEventHandlerActor(), device, touchSequence); - this._eventsGrab = grab; - } else { - Main.popModal(grab); - } - } - } - - _ungrabEvents() { - if (this._eventsGrab) { - this._ungrabDevice(); - Main.popModal(this._eventsGrab); - this._eventsGrab = null; - } - } - - _eventIsRelease(event) { - if (event.type() === Clutter.EventType.BUTTON_RELEASE) { - let buttonMask = Clutter.ModifierType.BUTTON1_MASK | - Clutter.ModifierType.BUTTON2_MASK | - Clutter.ModifierType.BUTTON3_MASK; - /* We only obey the last button release from the device, - * other buttons may get pressed/released during the DnD op. - */ - return (event.get_state() & buttonMask) === 0; - } else if (event.type() === Clutter.EventType.TOUCH_END) { - /* For touch, we only obey the pointer emulating sequence */ - return global.display.is_pointer_emulating_sequence(event.get_event_sequence()); + const grab = Main.pushModal(_getEventHandlerActor()); + if ((grab.get_seat_state() & Clutter.GrabState.POINTER) === 0) { + Main.popModal(grab); + return; } - return false; + this._grab = grab; + this._grabbedDevice = device; + this._grabbedEventSequence = touchSequence; } - _onEvent(actor, event) { - let device = event.get_device(); - - if (this._grabbedDevice && - device !== this._grabbedDevice && - device.get_device_type() !== Clutter.InputDeviceType.KEYBOARD_DEVICE) - return Clutter.EVENT_PROPAGATE; - - // We intercept BUTTON_RELEASE event to know that the button was released in case we - // didn't start the drag, to drop the draggable in case the drag was in progress, and - // to complete the drag and ensure that whatever happens to be under the pointer does - // not get triggered if the drag was cancelled with Esc. - if (this._eventIsRelease(event)) { - if (this._dragState === DragState.DRAGGING) { - return this._dragActorDropped(event); - } else if ((this._dragActor != null || this._dragState === DragState.CANCELLED) && - !this._animationInProgress) { - // Drag must have been cancelled with Esc. - this._dragComplete(); - return Clutter.EVENT_STOP; - } else { - // Drag has never started. - this._ungrabActor(); - return Clutter.EVENT_PROPAGATE; - } - // We intercept MOTION event to figure out if the drag has started and to draw - // this._dragActor under the pointer when dragging is in progress - } else if (event.type() === Clutter.EventType.MOTION || - (event.type() === Clutter.EventType.TOUCH_UPDATE && - global.display.is_pointer_emulating_sequence(event.get_event_sequence()))) { - if (this._dragActor && this._dragState === DragState.DRAGGING) - return this._updateDragPosition(event); - else if (this._dragActor == null && this._dragState !== DragState.CANCELLED) - return this._maybeStartDrag(event); - - // We intercept KEY_PRESS event so that we can process Esc key press to cancel - // dragging and ignore all other key presses. - } else if (event.type() === Clutter.EventType.KEY_PRESS && this._dragState === DragState.DRAGGING) { - let symbol = event.get_key_symbol(); - if (symbol === Clutter.KEY_Escape) { - this._cancelDrag(event.get_time()); - return Clutter.EVENT_STOP; - } - } - - return Clutter.EVENT_PROPAGATE; + _ungrabEvents() { + Main.popModal(this._grab); + this._grab = null; + this._grabbedDevice = null; + this._grabbedEventSequence = null; } /** @@ -339,56 +141,30 @@ class _Draggable extends Signals.EventEmitter { * PopupMenu.ignoreRelease()) */ fakeRelease() { - this._ungrabActor(); + this._dndGesture.set_state(Clutter.GestureState.CANCELLED); } - /** - * Directly initiate a drag and drop operation from the given actor. - * This function is useful to call if you've specified manualMode - * for the draggable. - * - * @param {number} stageX - X coordinate of event - * @param {number} stageY - Y coordinate of event - * @param {number} time - Event timestamp - * @param {Clutter.EventSequence=} sequence - Event sequence - * @param {Clutter.InputDevice=} device - device that originated the event - */ - startDrag(stageX, stageY, time, sequence, device) { - if (currentDraggable) - return; - - if (device === undefined) { - let event = Clutter.get_current_event(); + _gestureRecognized() { + const pointBeginEvent = this._dndGesture.get_point_begin_event(); + [this._dragStartX, this._dragStartY] = pointBeginEvent.get_coords(); - if (event) - device = event.get_device(); - - if (device === undefined) { - const backend = this.actor.get_context().get_backend(); - const seat = backend.get_default_seat(); - device = seat.get_pointer(); - } - } + const triggeringEvent = this._dndGesture.get_drag_triggering_event(); + const [stageX, stageY] = triggeringEvent.get_coords(); + const device = triggeringEvent.get_device(); + const sequence = triggeringEvent.get_event_sequence(); + const time = triggeringEvent.get_time(); currentDraggable = this; - this._dragState = DragState.DRAGGING; - // Special-case St.Button: the pointer grab messes with the internal - // state, so force a reset to a reasonable state here - if (this.actor instanceof St.Button) { - this.actor.fake_release(); - this.actor.hover = false; - } + this._grabEvents(device, sequence); + if (!this._grab) + return; this.emit('drag-begin', time); - if (this._onEventId) - this._ungrabActor(); - - this._grabEvents(device, sequence); global.display.set_cursor(Meta.Cursor.NO_DROP); - this._dragX = this._dragStartX = stageX; - this._dragY = this._dragStartY = stageY; + this._dragX = stageX; + this._dragY = stageY; let scaledWidth, scaledHeight; @@ -408,13 +184,13 @@ class _Draggable extends Signals.EventEmitter { // around the pointer let [sourceX, sourceY] = this._dragActorSource.get_transformed_position(); let x, y; - if (stageX > sourceX && stageX <= sourceX + this._dragActor.width && - stageY > sourceY && stageY <= sourceY + this._dragActor.height) { + if (this._dragStartX > sourceX && this._dragStartX <= sourceX + this._dragActor.width && + this._dragStartY > sourceY && this._dragStartY <= sourceY + this._dragActor.height) { x = sourceX; y = sourceY; } else { - x = stageX - this._dragActor.width / 2; - y = stageY - this._dragActor.height / 2; + x = this._dragStartX - this._dragActor.width / 2; + y = this._dragStartY - this._dragActor.height / 2; } this._dragActor.set_position(x, y); @@ -435,6 +211,7 @@ class _Draggable extends Signals.EventEmitter { this._dragActorSource = undefined; this._dragOrigParent = this.actor.get_parent(); + this._dragActorHadFixedPos = this._dragActor.fixed_position_set; this._dragOrigX = this._dragActor.allocation.x1; this._dragOrigY = this._dragActor.allocation.y1; @@ -474,8 +251,9 @@ class _Draggable extends Signals.EventEmitter { this._finishAnimation(); this._dragActor = null; - if (this._dragState === DragState.DRAGGING) - this._dragState = DragState.CANCELLED; + + if (this._dragCancellable) + this._cancelDrag(global.get_current_time()); }); this._dragOrigOpacity = this._dragActor.opacity; if (this._dragActorOpacity !== undefined) @@ -535,38 +313,6 @@ class _Draggable extends Signals.EventEmitter { this._dragY + this._dragOffsetY); } - _maybeStartDrag(event) { - let [stageX, stageY] = event.get_coords(); - - if (this._dragThresholdIgnored) - return Clutter.EVENT_PROPAGATE; - - // See if the user has moved the mouse enough to trigger a drag - let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; - let threshold = St.Settings.get().drag_threshold * scaleFactor; - if (!currentDraggable && - (Math.abs(stageX - this._dragStartX) > threshold || - Math.abs(stageY - this._dragStartY) > threshold)) { - const deviceType = event.get_source_device().get_device_type(); - const isPointerOrTouchpad = - deviceType === Clutter.InputDeviceType.POINTER_DEVICE || - deviceType === Clutter.InputDeviceType.TOUCHPAD_DEVICE; - const ellapsedTime = event.get_time() - this._dragStartTime; - - // Pointer devices (e.g. mouse) start the drag immediately - if (isPointerOrTouchpad || ellapsedTime > this._dragTimeoutThreshold) { - this.startDrag(stageX, stageY, event.get_time(), this._touchSequence, event.get_device()); - this._updateDragPosition(event); - } else { - this._dragThresholdIgnored = true; - this._ungrabActor(); - return Clutter.EVENT_PROPAGATE; - } - } - - return Clutter.EVENT_STOP; - } - _pickTargetActor() { return this._dragActor.get_stage().get_actor_at_pos( Clutter.PickMode.ALL, this._dragX, this._dragY); @@ -671,7 +417,8 @@ class _Draggable extends Signals.EventEmitter { switch (dropFunc(dropEvent)) { case DragDropResult.FAILURE: case DragDropResult.SUCCESS: - return true; + return; + case DragDropResult.CONTINUE: continue; } @@ -700,25 +447,23 @@ class _Draggable extends Signals.EventEmitter { if (this._dragActor && this._dragActor.get_parent() === Main.uiGroup) { if (this._restoreOnSuccess) { this._restoreDragActor(event.get_time()); - return true; + return; } else { this._dragActor.destroy(); } } - this._dragState = DragState.INIT; global.display.set_cursor(Meta.Cursor.DEFAULT); this.emit('drag-end', event.get_time(), true); this._dragComplete(); - return true; + return; } } target = target.get_parent(); } + // If no target has been found, cancel the drag this._cancelDrag(event.get_time()); - - return true; } _getRestoreLocation() { @@ -749,34 +494,7 @@ class _Draggable extends Signals.EventEmitter { return [x, y, scale]; } - _cancelDrag(eventTime) { - this.emit('drag-cancelled', eventTime); - let wasCancelled = this._dragState === DragState.CANCELLED; - this._dragState = DragState.CANCELLED; - - if (this._actorDestroyed || wasCancelled) { - global.display.set_cursor(Meta.Cursor.DEFAULT); - this._dragComplete(); - this.emit('drag-end', eventTime, false); - if (!this._dragOrigParent && this._dragActor) - this._dragActor.destroy(); - - return; - } - - let [snapBackX, snapBackY, snapBackScale] = this._getRestoreLocation(); - - this._animateDragEnd(eventTime, { - x: snapBackX, - y: snapBackY, - scale_x: snapBackScale, - scale_y: snapBackScale, - duration: SNAP_BACK_ANIMATION_TIME, - }); - } - _restoreDragActor(eventTime) { - this._dragState = DragState.INIT; let [restoreX, restoreY, restoreScale] = this._getRestoreLocation(); // fade the actor back in at its original location @@ -797,8 +515,8 @@ class _Draggable extends Signals.EventEmitter { ...params, opacity: this._dragOrigOpacity, mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => { - this._onAnimationComplete(this._dragActor, eventTime); + onStopped: () => { + this._onAnimationComplete(eventTime); }, }); } @@ -813,21 +531,21 @@ class _Draggable extends Signals.EventEmitter { global.display.set_cursor(Meta.Cursor.DEFAULT); } - _onAnimationComplete(dragActor, eventTime) { + _onAnimationComplete(eventTime) { if (this._dragOrigParent) { Main.uiGroup.remove_child(this._dragActor); this._dragOrigParent.add_child(this._dragActor); - dragActor.set_scale(this._dragOrigScale, this._dragOrigScale); + this._dragActor.set_scale(this._dragOrigScale, this._dragOrigScale); if (this._dragActorHadFixedPos) - dragActor.set_position(this._dragOrigX, this._dragOrigY); + this._dragActor.set_position(this._dragOrigX, this._dragOrigY); else - dragActor.fixed_position_set = false; + this._dragActor.fixed_position_set = false; if (this._dragActorHadNatWidth) this._dragActor.set_width(-1); if (this._dragActorHadNatHeight) this._dragActor.set_height(-1); } else { - dragActor.destroy(); + this._dragActor?.destroy(); } this.emit('drag-end', eventTime, false); @@ -835,10 +553,11 @@ class _Draggable extends Signals.EventEmitter { } _dragComplete() { - if (!this._actorDestroyed && this._dragActor) + if (this._dragActor) Shell.util_set_hidden_from_pick(this._dragActor, false); - this._ungrabEvents(); + if (this._grab) + this._ungrabEvents(); if (this._updateHoverId) { GLib.source_remove(this._updateHoverId); @@ -859,9 +578,61 @@ class _Draggable extends Signals.EventEmitter { this._dragActorSource.disconnect(this._dragActorSourceDestroyId); this._dragActorSource = null; } + } + + _onEvent(actor, event) { + if (event.type() === Clutter.EventType.KEY_PRESS) { + if (event.get_key_symbol() === Clutter.KEY_Escape) + this._cancelDrag(event.get_time()); + + return Clutter.EVENT_PROPAGATE; + } + + if (event.get_device() !== this._grabbedDevice) + return Clutter.EVENT_PROPAGATE; + + const sequence = event.get_event_sequence(); + if (this._grabbedEventSequence && + sequence?.get_slot() !== this._grabbedEventSequence.get_slot()) + return Clutter.EVENT_PROPAGATE; + + if (event.type() === Clutter.EventType.BUTTON_RELEASE || + event.type() === Clutter.EventType.TOUCH_END) + this._dragActorDropped(event); + + if (event.type() === Clutter.EventType.MOTION || + event.type() === Clutter.EventType.TOUCH_UPDATE) + this._updateDragPosition(event); + + return Clutter.EVENT_PROPAGATE; + } + + _cancelDrag(eventTime) { + this.emit('drag-cancelled', eventTime); + + if (!this._dragActor) { + global.display.set_cursor(Meta.Cursor.DEFAULT); + this._dragComplete(); + this.emit('drag-end', eventTime, false); + if (!this._dragOrigParent && this._dragActor) + this._dragActor.destroy(); + + return; + } + + let [snapBackX, snapBackY, snapBackScale] = this._getRestoreLocation(); + + this._animateDragEnd(eventTime, { + x: snapBackX, + y: snapBackY, + scale_x: snapBackScale, + scale_y: snapBackScale, + duration: SNAP_BACK_ANIMATION_TIME, + }); + } - this._dragState = DragState.INIT; - currentDraggable = null; + get startGesture() { + return this._dndGesture; } } diff --git a/js/ui/windowPreview.js b/js/ui/windowPreview.js index 6e1593de8..527a2a9c9 100644 --- a/js/ui/windowPreview.js +++ b/js/ui/windowPreview.js @@ -123,6 +123,8 @@ export const WindowPreview = GObject.registerClass({ longPressGesture.connect('recognize', () => this.showOverlay(true)); this.add_action(longPressGesture); + longPressGesture.can_not_cancel(this._draggable.startGesture); + this._overlayEnabled = true; this._overlayShown = false; this._closeRequested = false;